/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.code.motoo.core;

import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.LongBuffer;
import java.nio.ShortBuffer;

/**
 * 数据缓冲器的实现类，对ByteBuffer进行了包装，实现了数据自动增长和删减的功能
 * @author linfengqi
 * @version 1.0  May 12, 2009
 */
public class IoBuffer {

	boolean isAutoExpand;
	boolean isDirect;
	int initialCapacity;
	ByteBuffer bf;

	public IoBuffer(ByteBuffer bf, int capacity, boolean isAutoExpand, boolean isDirect) {
		this.bf = bf;
		this.isDirect = isDirect;
		this.initialCapacity = capacity;
		this.isAutoExpand = isAutoExpand;
	}

	public static IoBuffer allocate(int capacity) {
		return new IoBuffer(ByteBuffer.allocate(capacity), capacity, false, false);
	}

	public static IoBuffer allocate(int capacity, boolean isAutoExpand) {
		return new IoBuffer(ByteBuffer.allocate(capacity), capacity, isAutoExpand, false);
	}

	public static IoBuffer allocateDirect(int capacity) {
		return new IoBuffer(ByteBuffer.allocateDirect(capacity), capacity, false, true);
	}

	public static IoBuffer allocateDirect(int capacity, boolean isAutoExpand) {
		return new IoBuffer(ByteBuffer.allocateDirect(capacity), capacity, isAutoExpand,
				true);
	}
	
	public static IoBuffer wrap(byte[] array) {
		return new IoBuffer(ByteBuffer.wrap(array), array.length, false, false);
	}
	
	public static IoBuffer wrap(byte[] array, int offset, int length) {
		return new IoBuffer(ByteBuffer.wrap(array, offset, length), length, false, false);
	}

	public final int capacity() {
		return bf.capacity();
	}

	public final int position() {
		return bf.position();
	}

	public final void position(int newPosition) {
		bf.position(newPosition);
	}
	
	public final void recover() {
		bf.position(bf.limit());
		bf.limit(bf.capacity());
	}
	
	public final void compact() {
		bf.compact();
	}

	public final int limit() {
		return bf.limit();
	}

	public final void limit(int newLimit) {
		bf.limit(newLimit);
	}

	public final void mark() {
		bf.mark();
	}

	public final void reset() {
		bf.reset();
	}

	public final void clear() {
		bf.clear();
	}

	public final void flip() {
		bf.flip();
	}

	public final void rewind() {
		bf.rewind();
	}

	public final int remaining() {
		return bf.remaining();
	}

	public final boolean hasRemaining() {
		return bf.hasRemaining();
	}

	public byte get() {
		return bf.get();
	}

	public byte get(int i) {
		return bf.get(i);
	}

	public void get(byte[] dst, int offset, int length) {
		bf.get(dst, offset, length);
	}

	public void put(byte x) {
		ensureCapacity(position() + 1);
		bf.put(x);
	}

	public void put(int i, byte x) {
		ensureCapacity(position() + 1);
		bf.put(i, x);
	}
	
	public void put(byte[] src) {
		put(src, 0, src.length);
	}

	public void put(byte[] src, int offset, int length) {
		ensureCapacity(position() + length);
		bf.put(src, offset, length);
	}

	public void put(IoBuffer src) {
		int n = src.remaining();
		ensureCapacity(position() + n);
		bf.put(src.bf);
	}
	
	public void put(IoBuffer src, int length) {
		ensureCapacity(position() + length);
		
		if (length > remaining())
		    throw new BufferOverflowException();
		for (int i = 0; i < length; i++)
		    put(src.get());
	}

	public void put(IoBuffer src, int offset, int length) {
		ensureCapacity(position() + length);
		
		if (length > remaining())
		    throw new BufferOverflowException();
		for (int i = 0; i < length; i++)
		    put(src.get(offset + i));
	}
	
	public void put(ByteBuffer src) {
		int n = src.remaining();
		ensureCapacity(position() + n);
		bf.put(src);
	}

	public void put(ByteBuffer src, int offset, int length) {
		ensureCapacity(position() + length);
		
		if (length > remaining())
		    throw new BufferOverflowException();
		for (int i = 0; i < length; i++)
		    put(src.get(offset + i));
	}
	
	public byte[] array() {
		return bf.array();
	}
	
	public void trim(int offset, int length) {
		int fromIndex = offset;
		int toIndex = offset + length;
		RangeCheck(toIndex);
        
		byte[] source = bf.array();
		int oldPosition = bf.position();
		bf.position(fromIndex);
		bf.put(source, toIndex, bf.limit() - toIndex);
		int newPosition = oldPosition - length;
		if (newPosition < fromIndex) newPosition = fromIndex;
		bf.position(newPosition);
		bf.limit(bf.capacity());
	}
	
	private void RangeCheck(int index) {
		if (index >= bf.capacity())
			throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + bf.capacity());
	}
	
	public ByteBuffer asByteBuffer() {
		return bf;
	}
	
	public ByteBuffer buf() {
		return bf;
	}

	// char
	public char getChar() {
		return bf.getChar();
	}

	public char getChar(int i) {
		return bf.getChar(i);
	}

	public void putChar(char x) {
		ensureCapacity(position() + 2);
		bf.putChar(x);
	}

	public void putChar(int i, char x) {
		ensureCapacity(position() + 2);
		bf.putChar(i, x);
	}

	public CharBuffer asCharBuffer() {
		return bf.asCharBuffer();
	}

	// short

	public final short getUnsigned(int index) {
        return (short) (get(index) & 0xff);
    }
	
	public short getShort() {
		return bf.getShort();
	}

	public short getShort(int i) {
		return bf.getShort(i);
	}

	public void putShort(short value) {
		ensureCapacity(position() + 2);
		bf.putShort(value);
	}

	public void putShort(int index, short value) {
		ensureCapacity(position() + 2);
		bf.putShort(index, value);
	}
	
	public int getUnsignedShort() {
        return getShort() & 0xffff;
    }

    public int getUnsignedShort(int index) {
        return getShort(index) & 0xffff;
    }

	public ShortBuffer asShortBuffer() {
		return bf.asShortBuffer();
	}

	// int

	public int getInt() {
		return bf.getInt();
	}

	public int getInt(int index) {
		return bf.getInt(index);
	}

	public void putInt(int value) {
		ensureCapacity(position() + 4);
		bf.putInt(value);
	}

	public void putInt(int index, int value) {
		ensureCapacity(position() + 4);
		bf.putInt(index, value);
	}

	public IntBuffer asIntBuffer() {
		return bf.asIntBuffer();
	}

	// long

	public long getLong() {
		return bf.getLong();
	}

	public long getLong(int index) {
		return bf.getLong(index);
	}

	public void putLong(long value) {
		ensureCapacity(position() + 8);
		bf.putLong(value);
	}

	public void putLong(int index, int value) {
		ensureCapacity(position() + 8);
		bf.putLong(index, value);
	}

	public LongBuffer asLongBuffer() {
		return bf.asLongBuffer();
	}

	// float

	public float getFloat() {
		return bf.getFloat();
	}

	public float getFloat(int index) {
		return bf.getFloat(index);
	}

	public void putFloat(float value) {
		ensureCapacity(position() + 4);
		bf.putFloat(value);
	}

	public void putFloat(int index, int value) {
		ensureCapacity(position() + 4);
		bf.putFloat(index, value);
	}

	public FloatBuffer asFloatBuffer() {
		return bf.asFloatBuffer();
	}

	// double

	public double getDouble() {
		return bf.getDouble();
	}

	public double getDouble(int index) {
		return bf.getDouble(index);
	}

	public void putDouble(double value) {
		ensureCapacity(position() + 8);
		bf.putDouble(value);
	}

	public void putDouble(int index, double value) {
		ensureCapacity(position() + 8);
		bf.putDouble(index, value);
	}

	public DoubleBuffer asDoubleBuffer() {
		return bf.asDoubleBuffer();
	}
	
	public boolean prefixedDataAvailable(int prefixLength) {
        return prefixedDataAvailable(prefixLength, Integer.MAX_VALUE);
    }

    public boolean prefixedDataAvailable(int prefixLength, int maxDataLength) {
        if (remaining() < prefixLength) {
            return false;
        }

        int dataLength;
        switch (prefixLength) {
        case 1:
            dataLength = getUnsigned(position());
            break;
        case 2:
            dataLength = getUnsignedShort(position());
            break;
        case 4:
            dataLength = getInt(position());
            break;
        default:
            throw new IllegalArgumentException("prefixLength: " + prefixLength);
        }

        if (dataLength < 0 || dataLength > maxDataLength) {
            throw new BufferDataException("dataLength: " + dataLength);
        }

        return remaining() - prefixLength >= dataLength;
    }

	private void ensureCapacity(int minCapacity) {
		int oldCapacity = capacity();
		if (minCapacity > oldCapacity) {
			if (isAutoExpand) {
				ByteBuffer oldData = bf;
				int newCapacity = (oldCapacity * 3) / 2 + 1;
				if (newCapacity < minCapacity)
					newCapacity = minCapacity;
				bf = (isDirect) ? ByteBuffer.allocateDirect(newCapacity) : ByteBuffer.allocate(newCapacity);
				byte[] source = oldData.array();
				bf.put(source, 0, oldData.position());
			} else
				throw new BufferOverflowException();
		}
	}

	public void setAutoExpand(boolean autoExpand) {
		isAutoExpand = autoExpand;
	}

	public boolean isAutoExpand(boolean autoExpand) {
		return isAutoExpand;
	}

}
